React Error Boundaryã§ãšã©ãŒã广çã«åé¡ã»åŠçããæ¹æ³ãåŠã³ãã¢ããªã®å®å®æ§ãšUXãåäžãããŸãããã
React Error Boundaryã®ãšã©ãŒåé¡ïŒå®å šã¬ã€ã
ãšã©ãŒãã³ããªã³ã°ã¯ãå ç¢ã§ä¿å®æ§ã®é«ãReactã¢ããªã±ãŒã·ã§ã³ãæ§ç¯ããäžã§éåžžã«éèŠãªåŽé¢ã§ããReactã®Error Boundaryã¯ãã¬ã³ããªã³ã°äžã«çºçãããšã©ãŒãé©åã«åŠçããã¡ã«ããºã ãæäŸããŸãããçã«å埩åã®ããã¢ããªã±ãŒã·ã§ã³ãäœæããããã«ã¯ãããŸããŸãªãšã©ãŒã¿ã€ããã©ã®ããã«åé¡ãã察å¿ããããçè§£ããããšãäžå¯æ¬ ã§ãããã®ã¬ã€ãã§ã¯ãError Boundaryå ã§ã®ãšã©ãŒåé¡ã«å¯Ÿããæ§ã ãªã¢ãããŒããæ¢ãããšã©ãŒç®¡çæŠç¥ãæ¹åããããã®å®è·µçãªäŸãšå®çšçãªç¥èŠãæäŸããŸãã
React Error Boundaryãšã¯ïŒ
React 16ã§å°å
¥ãããError Boundaryã¯ãåã³ã³ããŒãã³ãããªãŒå
ã®ã©ããã§çºçããJavaScriptãšã©ãŒããã£ãããããããã®ãšã©ãŒããã°ã«èšé²ããã³ã³ããŒãã³ãããªãŒå
šäœãã¯ã©ãã·ã¥ããã代ããã«ãã©ãŒã«ããã¯UIã衚瀺ããReactã³ã³ããŒãã³ãã§ãããããã¯ã³ã³ããŒãã³ãçã®try...catchãããã¯ã®ããã«æ©èœããŸãã
Error Boundaryã®äž»ãªç¹åŸŽïŒ
- ã³ã³ããŒãã³ãã¬ãã«ã®ãšã©ãŒãã³ããªã³ã°ïŒ ç¹å®ã®ã³ã³ããŒãã³ããµãããªãŒå ã§ãšã©ãŒãåé¢ããŸãã
- ã°ã¬ãŒã¹ãã«ãã°ã©ããŒã·ã§ã³ïŒæ®µéçæ©èœäœäžïŒïŒ åäžã®ã³ã³ããŒãã³ããšã©ãŒã«ãã£ãŠã¢ããªã±ãŒã·ã§ã³å šäœãã¯ã©ãã·ã¥ããã®ãé²ããŸãã
- å¶åŸ¡ããããã©ãŒã«ããã¯UIïŒ ãšã©ãŒçºçæã«ããŠãŒã¶ãŒãã¬ã³ããªãŒãªã¡ãã»ãŒãžã代æ¿ã³ã³ãã³ãã衚瀺ããŸãã
- ãšã©ãŒãã®ã³ã°ïŒ ãšã©ãŒæ å ±ããã°ã«èšé²ããããšã§ããšã©ãŒã®è¿œè·¡ãšãããã°ã容æã«ããŸãã
ãªãError Boundaryã§ãšã©ãŒãåé¡ããã®ãïŒ
åã«ãšã©ãŒããã£ããããã ãã§ã¯äžååã§ãã广çãªãšã©ãŒãã³ããªã³ã°ã«ã¯ãäœãåé¡ã ã£ãã®ããçè§£ããããã«å¿ããŠå¯Ÿå¿ããããšãæ±ããããŸããError Boundaryå ã§ãšã©ãŒãåé¡ããããšã«ã¯ãããã€ãã®å©ç¹ããããŸãïŒ
- ã¿ãŒã²ãããçµã£ããšã©ãŒãã³ããªã³ã°ïŒ ãšã©ãŒã®çš®é¡ã«ãã£ãŠç°ãªã察å¿ãå¿ èŠã«ãªãå ŽåããããŸããäŸãã°ããããã¯ãŒã¯ãšã©ãŒã«ã¯å詊è¡ã¡ã«ããºã ããããŒã¿æ€èšŒãšã©ãŒã«ã¯ãŠãŒã¶ãŒå ¥åã®ä¿®æ£ãå¿ èŠã«ãªããããããŸããã
- ãŠãŒã¶ãŒãšã¯ã¹ããªãšã³ã¹ã®åäžïŒ ãšã©ãŒã®çš®é¡ã«åºã¥ããŠãããæ å ±éã®å€ããšã©ãŒã¡ãã»ãŒãžã衚瀺ããŸãããåé¡ãçºçããŸããããšããäžè¬çãªã¡ãã»ãŒãžãããããããã¯ãŒã¯ã®åé¡ãç¡å¹ãªå ¥åã瀺ãå ·äœçãªã¡ãã»ãŒãžã®æ¹ã圹ç«ã¡ãŸãã
- ãããã°ã®åŒ·åïŒ ãšã©ãŒãåé¡ããããšã§ããããã°ãåé¡ã®æ ¹æ¬åå ãç¹å®ããããã®è²Žéãªã³ã³ããã¹ããåŸãããŸãã
- ããã¢ã¯ãã£ããªç£èŠïŒ ããŸããŸãªãšã©ãŒã¿ã€ãã®çºçé »åºŠã远跡ããç¹°ãè¿ãçºçããåé¡ãç¹å®ããŠä¿®æ£ã®åªå é äœãä»ããŸãã
- æŠç¥çãªãã©ãŒã«ããã¯UIïŒ ãšã©ãŒã«å¿ããŠç°ãªããã©ãŒã«ããã¯UIã衚瀺ãããŠãŒã¶ãŒã«ããé¢é£æ§ã®é«ãæ å ±ãã¢ã¯ã·ã§ã³ãæäŸããŸãã
ãšã©ãŒåé¡ãžã®ã¢ãããŒã
React Error Boundaryå ã§ãšã©ãŒãåé¡ããããã«ãããã€ãã®ãã¯ããã¯ãå©çšã§ããŸãïŒ
1. instanceofã䜿çšãã
instanceofæŒç®åã¯ããªããžã§ã¯ããç¹å®ã®ã¯ã©ã¹ã®ã€ã³ã¹ã¿ã³ã¹ã§ãããã©ããããã§ãã¯ããŸããããã¯ãçµã¿èŸŒã¿ãŸãã¯ã«ã¹ã¿ã ã®ãšã©ãŒã¿ã€ãã«åºã¥ããŠãšã©ãŒãåé¡ããã®ã«åœ¹ç«ã¡ãŸãã
äŸïŒ
class NetworkError extends Error {
constructor(message) {
super(message);
this.name = "NetworkError";
}
}
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = "ValidationError";
}
}
class MyErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null, errorInfo: null };
}
static getDerivedStateFromError(error) {
// 次ã®ã¬ã³ããªã³ã°ã§ãã©ãŒã«ããã¯UIã衚瀺ãããããã«stateãæŽæ°ããŸãã
return { hasError: true, error: error };
}
componentDidCatch(error, errorInfo) {
// ãšã©ãŒå ±åãµãŒãã¹ã«ãšã©ãŒããã°èšé²ããããšãã§ããŸã
console.error("Caught error:", error, errorInfo);
this.setState({errorInfo: errorInfo});
}
render() {
if (this.state.hasError) {
// ä»»æã®ã«ã¹ã¿ã ãã©ãŒã«ããã¯UIãã¬ã³ããªã³ã°ã§ããŸã
let errorMessage = "åé¡ãçºçããŸããã";
if (this.state.error instanceof NetworkError) {
errorMessage = "ãããã¯ãŒã¯ãšã©ãŒãçºçããŸãããæ¥ç¶ã確èªããŠå詊è¡ããŠãã ããã";
} else if (this.state.error instanceof ValidationError) {
errorMessage = "æ€èšŒãšã©ãŒããããŸãããå
¥åã確èªããŠãã ããã";
}
return (
<div>
<h2>ãšã©ãŒïŒ</h2>
<p>{errorMessage}</p>
<details style={{ whiteSpace: 'pre-wrap' }}>
{this.state.error && this.state.error.toString()}<br />
{this.state.errorInfo.componentStack}
</details>
</div>
);
}
return this.props.children;
}
}
解説ïŒ
- çµã¿èŸŒã¿ã®
Errorã¯ã©ã¹ãæ¡åŒµããŠãã«ã¹ã¿ã ã®NetworkErrorããã³ValidationErrorã¯ã©ã¹ãå®çŸ©ãããŠããŸãã MyErrorBoundaryã³ã³ããŒãã³ãã®renderã¡ãœããå ã§ãinstanceofæŒç®åã䜿çšããŠãã£ããããããšã©ãŒã®ã¿ã€ãããã§ãã¯ããŠããŸãã- ãšã©ãŒã®çš®é¡ã«åºã¥ããŠããã©ãŒã«ããã¯UIã«ç¹å®ã®ãšã©ãŒã¡ãã»ãŒãžã衚瀺ãããŸãã
2. ãšã©ãŒã³ãŒããŸãã¯ããããã£ã䜿çšãã
ããäžã€ã®ã¢ãããŒãã¯ããšã©ãŒãªããžã§ã¯ãèªäœã«ãšã©ãŒã³ãŒããããããã£ãå«ããããšã§ããããã«ãããç¹å®ã®ãšã©ãŒã·ããªãªã«åºã¥ããŠããããã现ããåé¡ãå¯èœã«ãªããŸãã
äŸïŒ
function fetchData(url) {
return new Promise((resolve, reject) => {
fetch(url)
.then(response => {
if (!response.ok) {
const error = new Error("Network request failed");
error.code = response.status; // ã«ã¹ã¿ã ãšã©ãŒã³ãŒãã远å
reject(error);
}
return response.json();
})
.then(data => resolve(data))
.catch(error => reject(error));
});
}
class MyErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null, errorInfo: null };
}
static getDerivedStateFromError(error) {
// 次ã®ã¬ã³ããªã³ã°ã§ãã©ãŒã«ããã¯UIã衚瀺ãããããã«stateãæŽæ°ããŸãã
return { hasError: true, error: error };
}
componentDidCatch(error, errorInfo) {
// ãšã©ãŒå ±åãµãŒãã¹ã«ãšã©ãŒããã°èšé²ããããšãã§ããŸã
console.error("Caught error:", error, errorInfo);
this.setState({errorInfo: errorInfo});
}
render() {
if (this.state.hasError) {
let errorMessage = "åé¡ãçºçããŸããã";
if (this.state.error.code === 404) {
errorMessage = "ãªãœãŒã¹ãèŠã€ãããŸããã§ããã";
} else if (this.state.error.code >= 500) {
errorMessage = "ãµãŒããŒãšã©ãŒã§ããåŸã§ããäžåºŠã詊ããã ããã";
}
return (
<div>
<h2>ãšã©ãŒïŒ</h2>
<p>{errorMessage}</p>
<details style={{ whiteSpace: 'pre-wrap' }}>
{this.state.error && this.state.error.toString()}<br />
{this.state.errorInfo.componentStack}
</details>
</div>
);
}
return this.props.children;
}
}
解説ïŒ
fetchData颿°ã¯ãHTTPã¹ããŒã¿ã¹ã³ãŒãã衚ãcodeããããã£ããšã©ãŒãªããžã§ã¯ãã«è¿œå ããŸããMyErrorBoundaryã³ã³ããŒãã³ãã¯ãcodeããããã£ããã§ãã¯ããŠç¹å®ã®ãšã©ãŒã·ããªãªã倿ããŸãã- ãšã©ãŒã³ãŒãã«åºã¥ããŠç°ãªããšã©ãŒã¡ãã»ãŒãžã衚瀺ãããŸãã
3. äžå€®éæš©çãªãšã©ãŒãããã³ã°ã䜿çšãã
è€éãªã¢ããªã±ãŒã·ã§ã³ã§ã¯ãäžå€®éæš©çãªãšã©ãŒãããã³ã°ãç¶æããããšã§ãã³ãŒãã®æŽçãšä¿å®æ§ãåäžããŸããããã¯ããšã©ãŒã®ã¿ã€ããã³ãŒããç¹å®ã®ãšã©ãŒã¡ãã»ãŒãžãåŠçããžãã¯ã«ãããã³ã°ããèŸæžããªããžã§ã¯ããäœæããããšãå«ã¿ãŸãã
äŸïŒ
const errorMap = {
"NETWORK_ERROR": {
message: "ãããã¯ãŒã¯ãšã©ãŒãçºçããŸãããæ¥ç¶ã確èªããŠãã ããã",
retry: true,
},
"INVALID_INPUT": {
message: "å
¥åãç¡å¹ã§ããããŒã¿ã確èªããŠãã ããã",
retry: false,
},
404: {
message: "ãªãœãŒã¹ãèŠã€ãããŸããã§ããã",
retry: false,
},
500: {
message: "ãµãŒããŒãšã©ãŒã§ããåŸã§ããäžåºŠã詊ããã ããã",
retry: true,
},
"DEFAULT": {
message: "åé¡ãçºçããŸããã",
retry: false,
},
};
function handleCustomError(errorType) {
const errorDetails = errorMap[errorType] || errorMap["DEFAULT"];
return errorDetails;
}
class MyErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, errorDetails: null, errorInfo: null };
}
static getDerivedStateFromError(error) {
// 次ã®ã¬ã³ããªã³ã°ã§ãã©ãŒã«ããã¯UIã衚瀺ãããããã«stateãæŽæ°ããŸãã
const errorDetails = handleCustomError(error.message);
return { hasError: true, errorDetails: errorDetails };
}
componentDidCatch(error, errorInfo) {
// ãšã©ãŒå ±åãµãŒãã¹ã«ãšã©ãŒããã°èšé²ããããšãã§ããŸã
console.error("Caught error:", error, errorInfo);
this.setState({errorInfo: errorInfo});
}
render() {
if (this.state.hasError) {
const { message } = this.state.errorDetails;
return (
<div>
<h2>ãšã©ãŒïŒ</h2>
<p>{message}</p>
<details style={{ whiteSpace: 'pre-wrap' }}>
{this.state.errorDetails.message}<br />
{this.state.errorInfo.componentStack}
</details>
</div>
);
}
return this.props.children;
}
}
function MyComponent(){
const [data, setData] = React.useState(null);
React.useEffect(() => {
try {
throw new Error("NETWORK_ERROR");
} catch (e) {
throw e;
}
}, []);
return <div></div>;
}
解説ïŒ
errorMapãªããžã§ã¯ãã¯ããšã©ãŒã®ã¿ã€ããã³ãŒãã«åºã¥ããŠãã¡ãã»ãŒãžããªãã©ã€ãã©ã°ãªã©ã®ãšã©ãŒæ å ±ãæ ŒçŽããŸããhandleCustomError颿°ã¯ããšã©ãŒã¡ãã»ãŒãžã«åºã¥ããŠerrorMapãããšã©ãŒè©³çްãååŸããç¹å®ã®ã³ãŒããèŠã€ãããªãå Žåã¯ããã©ã«ãå€ãè¿ããŸããMyErrorBoundaryã³ã³ããŒãã³ãã¯handleCustomErrorã䜿çšããŠerrorMapããé©åãªãšã©ãŒã¡ãã»ãŒãžãååŸããŸãã
ãšã©ãŒåé¡ã®ãã¹ããã©ã¯ãã£ã¹
- æç¢ºãªãšã©ãŒã¿ã€ããå®çŸ©ããïŒ ã¢ããªã±ãŒã·ã§ã³çšã«äžè²«ãããšã©ãŒã¿ã€ããŸãã¯ã³ãŒãã®ã»ããã確ç«ããŸãã
- ã³ã³ããã¹ãæ å ±ãæäŸããïŒ ãããã°ã容æã«ããããã«ããšã©ãŒãªããžã§ã¯ãã«é¢é£ãã詳现æ å ±ãå«ããŸãã
- ãšã©ãŒåŠçããžãã¯ãäžå€®éæš©åããïŒ äžå€®éæš©çãªãšã©ãŒãããã³ã°ããŠãŒãã£ãªãã£é¢æ°ã䜿çšããŠããšã©ãŒåŠçãäžè²«ããŠç®¡çããŸãã
- ãšã©ãŒã广çã«ãã°èšé²ããïŒ ãšã©ãŒå ±åãµãŒãã¹ãšçµ±åããŠãæ¬çªç°å¢ã§ã®ãšã©ãŒã远跡ã»åæããŸãã人æ°ã®ãããµãŒãã¹ã«ã¯SentryãRollbarãBugsnagãªã©ããããŸãã
- ãšã©ãŒãã³ããªã³ã°ããã¹ãããïŒ Error BoundaryãããŸããŸãªãšã©ãŒã¿ã€ããæ£ããåŠçããããšã確èªããããã®åäœãã¹ããäœæããŸãã
- ãŠãŒã¶ãŒãšã¯ã¹ããªãšã³ã¹ãèæ ®ããïŒ ãŠãŒã¶ãŒã解決ã«å°ããæçã§åããããããšã©ãŒã¡ãã»ãŒãžã衚瀺ããŸããå°éçšèªã¯é¿ããŠãã ããã
- ãšã©ãŒçãç£èŠããïŒ ããŸããŸãªãšã©ãŒã¿ã€ãã®çºçé »åºŠã远跡ããç¹°ãè¿ãçºçããåé¡ãç¹å®ããŠä¿®æ£ã®åªå é äœãä»ããŸãã
- åœéå (i18n)ïŒ ãŠãŒã¶ãŒã«ãšã©ãŒã¡ãã»ãŒãžãæç€ºããéã¯ãã¡ãã»ãŒãžãããŸããŸãªèšèªãæåããµããŒãããããã«é©åã«åœéåãããŠããããšã確èªããŠãã ããã
i18nextã®ãããªã©ã€ãã©ãªãReactã®Context APIã䜿çšããŠç¿»èš³ã管çããŸãã - ã¢ã¯ã»ã·ããªã㣠(a11y)ïŒ ãšã©ãŒã¡ãã»ãŒãžãé害ã®ãããŠãŒã¶ãŒã«ãã¢ã¯ã»ã¹å¯èœã§ããããšã確èªããŠãã ãããARIA屿§ã䜿çšããŠãã¹ã¯ãªãŒã³ãªãŒããŒã«è¿œå ã®ã³ã³ããã¹ããæäŸããŸãã
- ã»ãã¥ãªãã£ïŒ ç¹ã«æ¬çªç°å¢ã§ã¯ããšã©ãŒã¡ãã»ãŒãžã«è¡šç€ºããæ å ±ã«æ³šæããŠãã ãããæ»æè ã«æªçšãããå¯èœæ§ã®ããæ©å¯ããŒã¿ãå ¬éããªãããã«ããŸãã äŸãã°ããšã³ããŠãŒã¶ãŒã«çã®ã¹ã¿ãã¯ãã¬ãŒã¹ã衚瀺ããªãã§ãã ããã
ã·ããªãªäŸïŒEã³ããŒã¹ã¢ããªã±ãŒã·ã§ã³ã§ã®APIãšã©ãŒã®åŠç
APIããååæ å ±ãååŸããEã³ããŒã¹ã¢ããªã±ãŒã·ã§ã³ãèããŠã¿ãŸããããèãããããšã©ãŒã·ããªãªã«ã¯ã以äžã®ãããªãã®ããããŸãïŒ
- ãããã¯ãŒã¯ãšã©ãŒïŒ APIãµãŒããŒãå©çšã§ããªãããŸãã¯ãŠãŒã¶ãŒã®ã€ã³ã¿ãŒãããæ¥ç¶ãäžæãããå Žåã
- èªèšŒãšã©ãŒïŒ ãŠãŒã¶ãŒã®èªèšŒããŒã¯ã³ãç¡å¹ãŸãã¯æéåãã®å Žåã
- ãªãœãŒã¹æªæ€åºãšã©ãŒïŒ èŠæ±ãããååãååšããªãå Žåã
- ãµãŒããŒãšã©ãŒïŒ APIãµãŒããŒãå éšãšã©ãŒã«ééããå Žåã
Error Boundaryãšãšã©ãŒåé¡ã䜿çšããããšã§ãã¢ããªã±ãŒã·ã§ã³ã¯ãããã®ã·ããªãªãé©åã«åŠçã§ããŸãïŒ
// äŸïŒç°¡ç¥çïŒ
async function fetchProduct(productId) {
try {
const response = await fetch(`/api/products/${productId}`);
if (!response.ok) {
if (response.status === 404) {
throw new Error("PRODUCT_NOT_FOUND");
} else if (response.status === 401 || response.status === 403) {
throw new Error("AUTHENTICATION_ERROR");
} else {
throw new Error("SERVER_ERROR");
}
}
return await response.json();
} catch (error) {
if (error instanceof TypeError && error.message === "Failed to fetch") {
throw new Error("NETWORK_ERROR");
}
throw error;
}
}
class ProductErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, errorDetails: null, errorInfo: null };
}
static getDerivedStateFromError(error) {
const errorDetails = handleCustomError(error.message); // åè¿°ã®errorMapã䜿çš
return { hasError: true, errorDetails: errorDetails };
}
componentDidCatch(error, errorInfo) {
console.error("Caught error:", error, errorInfo);
this.setState({errorInfo: errorInfo});
}
render() {
if (this.state.hasError) {
const { message, retry } = this.state.errorDetails;
return (
<div>
<h2>ãšã©ãŒïŒ</h2>
<p>{message}</p>
{retry && <button onClick={() => window.location.reload()}>å詊è¡</button>}
</div>
);
}
return this.props.children;
}
}
解説ïŒ
fetchProduct颿°ã¯ãAPIã¬ã¹ãã³ã¹ã®ã¹ããŒã¿ã¹ã³ãŒãããã§ãã¯ããã¹ããŒã¿ã¹ã«åºã¥ããŠç¹å®ã®ãšã©ãŒã¿ã€ããã¹ããŒããŸããProductErrorBoundaryã³ã³ããŒãã³ãã¯ãããã®ãšã©ãŒããã£ããããé©åãªãšã©ãŒã¡ãã»ãŒãžã衚瀺ããŸãã- ãããã¯ãŒã¯ãšã©ãŒããµãŒããŒãšã©ãŒã®å Žåããå詊è¡ããã¿ã³ã衚瀺ããããŠãŒã¶ãŒã¯ãªã¯ãšã¹ããå詊è¡ã§ããŸãã
- èªèšŒãšã©ãŒã®å ŽåããŠãŒã¶ãŒã¯ãã°ã€ã³ããŒãžã«ãªãã€ã¬ã¯ãããããããããŸããã
- ãªãœãŒã¹æªæ€åºãšã©ãŒã®å Žåãååãååšããªãããšã瀺ãã¡ãã»ãŒãžã衚瀺ãããŸãã
ãŸãšã
React Error Boundaryå
ã§ãšã©ãŒãåé¡ããããšã¯ãå埩åãããããŠãŒã¶ãŒãã¬ã³ããªãŒãªã¢ããªã±ãŒã·ã§ã³ãæ§ç¯ããããã«äžå¯æ¬ ã§ããinstanceofãã§ãã¯ããšã©ãŒã³ãŒããäžå€®éæš©çãªãšã©ãŒãããã³ã°ãªã©ã®ãã¯ããã¯ãæ¡çšããããšã§ãããŸããŸãªãšã©ãŒã·ããªãªã«å¹æçã«å¯ŸåŠããããè¯ããŠãŒã¶ãŒãšã¯ã¹ããªãšã³ã¹ãæäŸã§ããŸããã¢ããªã±ãŒã·ã§ã³ãäºæãã¬ç¶æ³ãé©åã«åŠçã§ããããã«ããšã©ãŒãã³ããªã³ã°ããã®ã³ã°ããã¹ãã®ãã¹ããã©ã¯ãã£ã¹ã«åŸãããšãå¿ããªãã§ãã ããã
ãããã®æŠç¥ãå®è£ ããããšã§ãReactã¢ããªã±ãŒã·ã§ã³ã®å®å®æ§ãšä¿å®æ§ãå€§å¹ ã«åäžããããŠãŒã¶ãŒã®å Žæãèæ¯ã«é¢ä¿ãªããããã¹ã ãŒãºã§ä¿¡é Œæ§ã®é«ãäœéšãæäŸã§ããŸãã
åèè³æïŒ